上一篇,我们介绍了指针以及指针和数组的关系的基本知识,本篇我们继续讲讲有关指针的其他知识。
函数、指针和数组
在传递数组作为函数参数时要特别注意传递的只是指针,如下:
#include <stdio.h> void testArrayParameter (int arr[]) ;int main (void ) { int arr[10 ] = {1 , 2 , 3 , 4 } printf ("%d" , sizeof arr); testArrayParameter(arr); return 0 ; }void testArrayParameter (int arr[]) { printf ("%d" , sizeof arr); }
在我们的系统中,指针占用 8 个字节。
int *arr 形式和 int arr[] 形式都表示 arr 是一个指向 int 的指针。但是 int arr[] 只能用于声明形式参数。因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针。只有在这种情况下,C 才会把 int arr[] 和 int * arr 解释成一样。由于函数原型可以省略参数名,所以下面 4 种原型都是等价的:
int sum(int *arr, int n);
int sum(int *, int);
int sum(int arr[], int n);
int sum(int [], int);
从以上分析可知,处理数组的函数实际上用指针作为参数,但是在编写这样的函数时,可以选择是使用数组表示法还是指针表示法。但是,只有当 arr 是指针变量时,才能使用 arr++ 这样的表达式。
指针操作
C 提供了一些基本的指针操作,我们接下来演示了 6 种不同的关于指针的操作。
#include <stdio.h> int main (void ) { int urn[5 ] = { 100 , 200 , 300 , 400 , 500 }; int * ptr1, *ptr2, *ptr3; ptr1 = urn; ptr2 = &urn[2 ]; printf ("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n" , ptr1, *ptr1, &ptr1); ptr3 = ptr1 + 4 ; printf ("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n" , ptr1 + 4 , *(ptr1 + 4 )); printf ("ptr3 = %p, ptr3 - 2 = %p\n" , ptr3, ptr3 - 2 ); ptr1++; printf ("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n" , ptr1, *ptr1, &ptr1); ptr2--; printf ("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n" , ptr2, *ptr2, &ptr2); printf ("ptr2 + 1 = %p, ptr1 = %p, ptr2 + 1 - ptr1 = %td\n" , ptr2 + 1 , ptr1, ptr2 + 1 - ptr1); return 0 ; }
下面是我们的系统运行该程序后的输出:
ptr1 = 0x7fff5fbff8d0 , *ptr1 =100 , &ptr1 = 0x7fff5fbff8c8 ptr1 + 4 = 0x7fff5fbff8e0 , *(ptr1 + 4 ) = 500 ptr3 = 0x7fff5fbff8e0 , ptr3 - 2 = 0x7fff5fbff8d8 ptr1 = 0x7fff5fbff8d4 , *ptr1 =200 , &ptr1 = 0x7fff5fbff8c8 ptr2 = 0x7fff5fbff8d4 , *ptr2 = 200 , &ptr2 = 0x7fff5fbff8c0 ptr2 = 0x7fff5fbff8d8 , ptr1 = 0x7fff5fbff8d4 , ptr2 - ptr1 = 1
形式参数使用 const
对于函数参数,通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递指针。而对于数组来说,必须传递指针。有时传递地址会导致一些问题,例如无意修改了源数据。在 K&R C 的年代,避免类似错误的唯一方法是提高警惕。ANSI C 提供了一种预防手段。如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字 const。
int sum (const int arr[], int n) ; int sum (const int arr[], int n) { int i; int total = 0 ; for ( i = 0 ; i < n; i++) total += arr[i]; return total; }
以上代码中的 const 告诉编译器,该函数不能修改 arr 指向的数组中的内容。如果在函数中不小心使用类似 arr[i]++ 的表达式,编译器会捕获这个错误,并生成一条错误信息。这里一定要理解,这样使用 const 并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。
一般而言,如果编写的函数需要修改数组,在声明数组形参时则不使用 const,如果编写的函数不用修改数组,那么在声明数组形参时最好使用 const。
const 其他内容 我们在前面使用 const 创建过变量:
const double PI = 3.14159 ;
虽然用 #define 指令可以创建类似功能的符号常量,但是 const 的用法更加灵活。可以创建 const 数组、const 指针和指向 const 的指针。
const int days[MONTHS] = {31 ,28 ,31 ,30 ,31 ,30 ,31 ,31 ,30 ,31 ,30 ,31 };
如果程序稍后尝试改变数组元素的值,编译器将生成一个编译期错误消息:
指向 const 的指针不能用于改变值,例如:
double rates[5 ] = {88.99 , 100.12 , 59.45 , 183.11 , 340.5 };const double * pd = rates; *pd = 29.89 ; pd[2 ] = 222.22 ; pd++;
指向 const 的指针通常用于函数形参中,表明该函数不会使用指针改变数据。
关于指针赋值和 const 需要注意一些规则。 首先,把 const 数据或非 const 数据的地址初始化为指向 const 的指针或为其赋值是合法的:
double rates[5 ] = {88.99 , 100.12 , 59.45 , 183.11 , 340.5 };const double locked[4 ] = {0.08 , 0.075 , 0.0725 , 0.07 };const double * pc = rates; pc = locked; pc = &rates[3 ];
然而,只能把非 const 数据的地址赋给普通指针:
double rates[5 ] = {88.99 , 100.12 , 59.45 , 183.11 , 340.5 };const double locked[4 ] = {0.08 , 0.075 , 0.0725 , 0.07 };double * pnc = rates; pnc = locked; pnc = &rates[3 ];
这个规则非常合理。否则,通过指针就能改变 const 数组中的数据。
const 还有其他的用法。例如,可以声明并初始化一个不能指向别处的指针,关键是 const 的位置:
double rates[5 ] = {88.99 , 100.12 , 59.45 , 183.11 , 340.5 };double * const pc = rates; pc = &rates[2 ]; *pc = 92.99 ;
最后,在创建指针时还可以使用 const 两次,使得该指针既不能更改它所指向的地址,也不能修改指向地址上的值:
double rates[5 ] = {88.99 , 100.12 , 59.45 , 183.11 , 340.5 };const double * const pc = rates; pc = &rates[2 ]; *pc = 92.99 ;
复合字面量
假设给带 int 类型形参的函数传递一个值,要传递 int 类型的变量,但是也可以传递 int 类型常量,如 5。在 C99 标准以前,对于带数组形参的函数,情况不同,可以传递数组,但是没有等价的数组常量。C99 新增了复合字面量(compound literal)。 对于数组,复合字面量类似数组初始化列表,前面是用括号括起来的类型名。例如:
int diva[2 ] = {10 , 20 }; (int [2 ]){10 , 20 }
初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数。 有了复合字面量,我们在把信息传入函数前不必先创建数组,这是复合字面量的典型用法。